/************************************************************************
* render_map.c
* voxelands - 3d voxel world sandbox game
* Copyright (C) Lisa 'darkrose' Milne 2016 <lisa@ltmnet.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program.  If not, see <http://www.gnu.org/licenses/>
************************************************************************/

#include "common.h"
#include "graphics.h"
#include "map.h"
#include "list.h"
#include "array.h"
#include "thread.h"

#include <math.h>

static struct {
	array_t *objects;
	mapobj_t *sorted;
	shader_t *shader;
	mutex_t *mutex;
	float frustum[6][4];
	matrix_t view;
} render_map_data = {
	NULL,
	NULL,
	NULL,
	NULL,
};

/*
 * LOD
 * 0 - detail overlay	0-24 blocks
 * 1 - standard LOD	0-48 blocks
 * 2 - low detail	48-96 blocks
 * 3 - distance		96+ blocks
 */

static int render_map_sort(void *e1, void *e2)
{
	mapobj_t *o1;
	mapobj_t *o2;

	o1 = e1;
	o2 = e2;

	/* return 1 if e1 is closer to the camera than e2 */
	if (o1->distance < o2->distance)
		return 1;
	return 0;
}

static void render_map_genvao(mesh_t *m)
{
	m->vao.list = 0;
	if (!m->v->length || !m->i->length)
		return;

	glGenVertexArrays(1,&m->vao.list);
	glBindVertexArray(m->vao.list);

	glGenBuffers(1, &m->vao.indices);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m->vao.indices);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, m->i->length*4, m->i->data, GL_STATIC_DRAW);

	glGenBuffers(1, &m->vao.vertices);
	glBindBuffer(GL_ARRAY_BUFFER, m->vao.vertices);
	glBufferData(GL_ARRAY_BUFFER, m->v->length*4, m->v->data, GL_STATIC_DRAW);
	glEnableVertexAttribArray(0);
	glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,0,0);

	glGenBuffers(1, &m->vao.normals);
	glBindBuffer(GL_ARRAY_BUFFER, m->vao.normals);
	glBufferData(GL_ARRAY_BUFFER, m->n->length*4, m->n->data, GL_STATIC_DRAW);
	glEnableVertexAttribArray(1);
	glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,0,0);

	glGenBuffers(1, &m->vao.texcoords);
	glBindBuffer(GL_ARRAY_BUFFER, m->vao.texcoords);
	glBufferData(GL_ARRAY_BUFFER, m->t->length*4, m->t->data, GL_STATIC_DRAW);
	glEnableVertexAttribArray(2);
	glVertexAttribPointer(2,2,GL_FLOAT,GL_FALSE,0,0);

	m->vao.state = 1;
}

static void render_map_calc_frustum(matrix_t *projection)
{
	matrix_t mat;
	float t;

	mat = *projection;
	matrix_multiply(&mat,&render_map_data.view);

	/* Extract the numbers for the RIGHT plane */
	render_map_data.frustum[0][0] = mat.data[ 3] - mat.data[ 0];
	render_map_data.frustum[0][1] = mat.data[ 7] - mat.data[ 4];
	render_map_data.frustum[0][2] = mat.data[11] - mat.data[ 8];
	render_map_data.frustum[0][3] = mat.data[15] - mat.data[12];

	/* Normalize the result */
	t = sqrt( render_map_data.frustum[0][0] * render_map_data.frustum[0][0] + render_map_data.frustum[0][1] * render_map_data.frustum[0][1] + render_map_data.frustum[0][2] * render_map_data.frustum[0][2] );
	render_map_data.frustum[0][0] /= t;
	render_map_data.frustum[0][1] /= t;
	render_map_data.frustum[0][2] /= t;
	render_map_data.frustum[0][3] /= t;

	/* Extract the numbers for the LEFT plane */
	render_map_data.frustum[1][0] = mat.data[ 3] + mat.data[ 0];
	render_map_data.frustum[1][1] = mat.data[ 7] + mat.data[ 4];
	render_map_data.frustum[1][2] = mat.data[11] + mat.data[ 8];
	render_map_data.frustum[1][3] = mat.data[15] + mat.data[12];

	/* Normalize the result */
	t = sqrt( render_map_data.frustum[1][0] * render_map_data.frustum[1][0] + render_map_data.frustum[1][1] * render_map_data.frustum[1][1] + render_map_data.frustum[1][2] * render_map_data.frustum[1][2] );
	render_map_data.frustum[1][0] /= t;
	render_map_data.frustum[1][1] /= t;
	render_map_data.frustum[1][2] /= t;
	render_map_data.frustum[1][3] /= t;

	/* Extract the BOTTOM plane */
	render_map_data.frustum[2][0] = mat.data[ 3] + mat.data[ 1];
	render_map_data.frustum[2][1] = mat.data[ 7] + mat.data[ 5];
	render_map_data.frustum[2][2] = mat.data[11] + mat.data[ 9];
	render_map_data.frustum[2][3] = mat.data[15] + mat.data[13];

	/* Normalize the result */
	t = sqrt( render_map_data.frustum[2][0] * render_map_data.frustum[2][0] + render_map_data.frustum[2][1] * render_map_data.frustum[2][1] + render_map_data.frustum[2][2] * render_map_data.frustum[2][2] );
	render_map_data.frustum[2][0] /= t;
	render_map_data.frustum[2][1] /= t;
	render_map_data.frustum[2][2] /= t;
	render_map_data.frustum[2][3] /= t;

	/* Extract the TOP plane */
	render_map_data.frustum[3][0] = mat.data[ 3] - mat.data[ 1];
	render_map_data.frustum[3][1] = mat.data[ 7] - mat.data[ 5];
	render_map_data.frustum[3][2] = mat.data[11] - mat.data[ 9];
	render_map_data.frustum[3][3] = mat.data[15] - mat.data[13];

	/* Normalize the result */
	t = sqrt( render_map_data.frustum[3][0] * render_map_data.frustum[3][0] + render_map_data.frustum[3][1] * render_map_data.frustum[3][1] + render_map_data.frustum[3][2] * render_map_data.frustum[3][2] );
	render_map_data.frustum[3][0] /= t;
	render_map_data.frustum[3][1] /= t;
	render_map_data.frustum[3][2] /= t;
	render_map_data.frustum[3][3] /= t;

	/* Extract the FAR plane */
	render_map_data.frustum[4][0] = mat.data[ 3] - mat.data[ 2];
	render_map_data.frustum[4][1] = mat.data[ 7] - mat.data[ 6];
	render_map_data.frustum[4][2] = mat.data[11] - mat.data[10];
	render_map_data.frustum[4][3] = mat.data[15] - mat.data[14];

	/* Normalize the result */
	t = sqrt( render_map_data.frustum[4][0] * render_map_data.frustum[4][0] + render_map_data.frustum[4][1] * render_map_data.frustum[4][1] + render_map_data.frustum[4][2] * render_map_data.frustum[4][2] );
	render_map_data.frustum[4][0] /= t;
	render_map_data.frustum[4][1] /= t;
	render_map_data.frustum[4][2] /= t;
	render_map_data.frustum[4][3] /= t;

	/* Extract the NEAR plane */
	render_map_data.frustum[5][0] = mat.data[ 3] + mat.data[ 2];
	render_map_data.frustum[5][1] = mat.data[ 7] + mat.data[ 6];
	render_map_data.frustum[5][2] = mat.data[11] + mat.data[10];
	render_map_data.frustum[5][3] = mat.data[15] + mat.data[14];

	/* Normalize the result */
	t = sqrt( render_map_data.frustum[5][0] * render_map_data.frustum[5][0] + render_map_data.frustum[5][1] * render_map_data.frustum[5][1] + render_map_data.frustum[5][2] * render_map_data.frustum[5][2] );
	render_map_data.frustum[5][0] /= t;
	render_map_data.frustum[5][1] /= t;
	render_map_data.frustum[5][2] /= t;
	render_map_data.frustum[5][3] /= t;
}

int render_map_point_in_frustum(v3_t *p)
{
	int i;
	for (i=0; i<6; i++) {
		if (((render_map_data.frustum[i][0]*p->x)+(render_map_data.frustum[i][1]*p->y)+(render_map_data.frustum[i][2]*p->z)+render_map_data.frustum[i][3]) <= 0)
			return 0;
	}
	return 1;
}

float render_map_obj_distance(mapobj_t *o, camera_t *cam)
{
	v3_t cp;
	v3_t op;

	cp.x = cam->x;
	cp.y = cam->y;
	cp.z = cam->z;

	/* base distance on the centre of the chunk */
	op.x = o->pos.x+8;
	op.y = o->pos.y+8;
	op.z = o->pos.z+8;

	o->distance = math_distance(&cp,&op);

	if (o->distance < 0.0)
		o->distance *= -1.0;

	o->lod = o->distance/24;
	if (o->lod < 0) {
		o->lod = 0;
	}else if (o->lod > 3) {
		o->lod = 3;
	}

	return o->distance;
}

int render_map_bounds_in_frustum(v3_t *p, aabox_t *b)
{
	v3_t v[8];
	int i;
	int k;

	if (render_map_point_in_frustum(p))
		return 1;

	v[0].x = b->min.x;
	v[0].y = b->min.y;
	v[0].z = b->min.z;

	v[1].x = b->max.x;
	v[1].y = b->min.y;
	v[1].z = b->min.z;

	v[2].x = b->min.x;
	v[2].y = b->max.y;
	v[2].z = b->min.z;

	v[3].x = b->max.x;
	v[3].y = b->max.y;
	v[3].z = b->min.z;

	v[4].x = b->min.x;
	v[4].y = b->min.y;
	v[4].z = b->max.z;

	v[5].x = b->max.x;
	v[5].y = b->min.y;
	v[5].z = b->max.z;

	v[6].x = b->min.x;
	v[6].y = b->max.y;
	v[6].z = b->max.z;

	v[7].x = b->max.x;
	v[7].y = b->max.y;
	v[7].z = b->max.z;

	for (i=0; i<6; i++) {
		for (k=0; k<8; k++) {
			if (((
				render_map_data.frustum[i][0]*v[k].x)
				+(render_map_data.frustum[i][1]*v[k].y)
				+(render_map_data.frustum[i][2]*v[k].z)
				+render_map_data.frustum[i][3]) > 0
			)
				break;
		}
		if (k == 8)
			return 0;
	}

	return 1;
}

int render_map_occlusion_test(v3_t *p0, v3_t *p1)
{
	float step = 1.0;
	float s;
	float d;
	v3_t u;
	pos_t p;
	block_t *b;
	contentfeatures_t *f;

	d = math_distance(p0,p1);
	if (d < 0)
		d *= -1.0;

	d -= 32.27;

	u.x = p1->x-p0->x;
	u.y = p1->y-p0->y;
	u.z = p1->z-p0->z;
	vect_normalise(&u);

	for (s=1.0; s<d; s+=step) {
		p.x = p0->x+(u.x*s);
		p.y = p0->y+(u.y*s);
		p.z = p0->z+(u.z*s);
		b = map_get_block(&p);
		if (b) {
			f = content_features(0);
			if (
				f && (
					f->draw_type == CDT_CUBELIKE
					|| f->draw_type == CDT_DIRTLIKE
					|| f->draw_type == CDT_MELONLIKE
				)
			)
				return 1;
		}

		step *= 1.9;
	}
	return 0;
}

int render_map_chunk_occluded(mapobj_t *o, camera_t *cam)
{
	v3_t cpn;
	v3_t spn;
	int i;
	v3_t offsets[9] = {
		{0.0,0.0,0.0},
		{9.0,9.0,9.0},
		{9.0,9.0,-9.0},
		{9.0,-9.0,9.0},
		{9.0,-9.0,-9.0},
		{-9.0,9.0,9.0},
		{-9.0,9.0,-9.0},
		{-9.0,-9.0,9.0},
		{-9.0,-9.0,-9.0},
	};

	spn.x = cam->x;
	spn.y = cam->y;
	spn.z = cam->z;

	for (i=0; i<9; i++) {
		cpn.x = o->pos.x+8.0+offsets[i].x;
		cpn.y = o->pos.y+8.0+offsets[i].y;
		cpn.z = o->pos.z+8.0+offsets[i].z;
		if (!render_map_occlusion_test(&spn,&cpn))
			return 0;
	}

	return 1;
}

/* render a mesh */
mapobj_t *render_map_chunk(mapobj_t *o)
{
	if (!render_map_data.objects) {
		render_map_data.mutex = mutex_create();
		render_map_data.objects = array_create(ARRAY_TYPE_PTR);
	}

	mutex_lock(render_map_data.mutex);
	array_push_ptr(render_map_data.objects,o);
	mutex_unlock(render_map_data.mutex);

	return o;
}

/* render 3d graphics to the frame */
void render_map(camera_t *cam, v4_t *plane)
{
	static v4_t p = {1000000.0,0.0,-1.0,0.0};
	int i;
	int l;
	mapobj_t *o;
	maplodobj_t *lo;
	mesh_t *m;
	matrix_t mat;
	matrix_t *projection;
	if (!render_map_data.objects || !render_map_data.objects->length)
		return;

	if (!render_map_data.shader) {
		render_map_data.shader = shader_create("map");
		shader_attribute(render_map_data.shader,0,"position");
		shader_attribute(render_map_data.shader,1,"normals");
		shader_attribute(render_map_data.shader,2,"texcoords");
		/* possibly a colours attribute as well */
	}

	shader_enable(render_map_data.shader);

	camera_view_matrix(&render_map_data.view,cam);

	projection = render_get_projection_matrix();

	shader_uniform_matrix(render_map_data.shader,"projectionMatrix",projection);
	shader_uniform_matrix(render_map_data.shader,"viewMatrix",&render_map_data.view);
	if (plane) {
		glEnable(GL_CLIP_DISTANCE0);
	}else{
		glDisable(GL_CLIP_DISTANCE0);
		plane = &p;
	}
	shader_uniform_v4(render_map_data.shader,"plane",plane);

	/* calculate the frustum */
	render_map_calc_frustum(projection);

	render_map_data.sorted = NULL;

	mutex_lock(render_map_data.mutex);
	for (i=0; i<render_map_data.objects->length; i++) {
		o = ((mapobj_t**)(render_map_data.objects->data))[i];
		/* check if we're meant to touch it */
		if (!o || !o->objs[1].meshes)
			continue;
		if (render_map_obj_distance(o,cam) > wm_data.distance)
			continue;
		if (!render_map_bounds_in_frustum(&o->pos,&o->bounds))
			continue;
		if (render_map_chunk_occluded(o,cam))
			continue;
		/* sort objects by distance, ignoring anything that can't be seen */
		render_map_data.sorted = list_insert_cmp(&render_map_data.sorted,o,render_map_sort);
	}
	mutex_unlock(render_map_data.mutex);

	o = render_map_data.sorted;

	/* now render chunks, furthest first */
	while (o) {
		matrix_init(&mat);
		matrix_translate_v(&mat,&o->pos);
		shader_uniform_matrix(render_map_data.shader,"transformationMatrix",&mat);
		/*
		light_bind_near(render_map_data.shader,&o->pos);
		*/

		l = o->lod;

		do {
			lo = &o->objs[l];

			if (lo && lo->meshes) {
				for (i=0; i<lo->meshes->length; i++) {
					m = ((mesh_t**)lo->meshes->data)[i];
					/* create buffers if necessary and fill with data */
					if (!m->vao.state)
						render_map_genvao(m);

					if (!m->vao.list)
						continue;

					glBindVertexArray(m->vao.list);
					glEnableVertexAttribArray(0);

					mat_use(m->mat,render_map_data.shader);

					glEnableVertexAttribArray(1);
					glEnableVertexAttribArray(2);
					glDrawElements(m->mode,m->i->length,GL_UNSIGNED_INT,NULL);
				}
			}
			l++;
		} while (l < 2);

		if (o->lod < 2) {
			/* TODO: render block objects */
			/* render now, or queue and instance render later? */
		}

		o = o->next;
	}

	glDisableVertexAttribArray(0);
	glDisableVertexAttribArray(1);
	glDisableVertexAttribArray(2);
	glBindVertexArray(0);

	shader_disable(render_map_data.shader);
}
